import math, random
import parole
import main, ai, language, sim
from sim import Event, Obj
from util import *
from parole.colornames import colors

class Item(sim.Obj):
    """
    Something that can be picked up and has a weight. If unidTemplate is
    non-null, then the item is part of the identification system.
    """
    def __init__(self, name, layer, shader, weight, startsVowel=False,
            unidTemplate=None, unidMeanings=None, unidDescription=None,
            idDescription=None):
        Obj.__init__(self, name, layer, shader, startsVowel=startsVowel,
                baseDescription=unidDescription)
        self.weight = weight
        self.carrier = None
        self.unidTemplate = unidTemplate
        self.unidMeanings = unidMeanings
        self.idDescription = idDescription

    def noun(self, definite=False, article=True, capitalized=False,
            nameOverride=None, plural=False, number=1, forceUnId=False,
            annotation=True, affixes=True):
        if self.unidTemplate and (not main.player.recognizes(self) or forceUnId):
            if self.unidMeanings:
                b = self.unidTemplate % \
                        {'form': main.player.language.getForm(self.unidMeanings),
                         'fg_rgb': ','.join([str(x) for x in self.shader.rgb])}
            else:
                b = self.unidTemplate
            return Obj.noun(self, definite, article, capitalized,
                    nameOverride=b, annotation=annotation, plural=plural,
                    number=number, affixes=affixes)
        else:
            return Obj.noun(self, definite, article, capitalized,
                    nameOverride=nameOverride, plural=plural, number=number,
                    annotation=annotation, affixes=affixes)

    def nounAnnotation(self):
        annotation = super(Item, self).nounAnnotation()
        if self.carrier is main.player:
            annotation += " {\DeepPink (%s lbs)}" % round(self.weight, 2)
        return annotation

    def unidForm(self):
        if self.unidMeanings:
            return main.player.language.getForm(self.unidMeanings)

    def description(self):
        if self.idDescription and main.player.recognizes(self):
            return self.idDescription
        else:
            return super(Item, self).description()

#=========================================

class Wearable(Item):
    """
    Something that can be worn on a wearslot.
    """
    bonusKeys = [
        'maxHPMod', 'strengthMod', 'dexterityMod', 'constitutionMod',
        'perceptionMod', 'speedMod', 'maxStaminaMod', 'staminaRegenMod',
        'capacityMod', 'pvMod', 'evMod', 'hitMod', 'damageSkewMod',
        'maxUnarmedDamageMod', 'minVisibleLightMod',
    ]

    def __init__(self, name, layer, shader, weight, wearSlots,
            wearEnergy, bonuses=None, startsVowel=False, unidTemplate=None,
            unidMeanings=None, unidDescription=None, idDescription=None,
            handsNeeded=0):
        Item.__init__(self, name, layer, shader, weight,
                startsVowel=startsVowel, unidTemplate=unidTemplate,
                unidMeanings=unidMeanings, unidDescription=unidDescription,
                idDescription=idDescription)
        self.wearSlots = list(wearSlots)
        self.wearer = None
        self.wearerSlot = None
        self.wearEnergy = wearEnergy
        self.handsNeeded = handsNeeded
        self.bonuses = dict(zip(self.bonusKeys, (0,)*len(self.bonusKeys)))
        if bonuses:
            if type(bonuses) not in (dict, None):
                raise TypeError('bonuses must be a dict or None')
            else:
                for k,v in bonuses.iteritems():
                    if k not in self.bonusKeys:
                        raise ValueError('Unknown bonus key %r' % k)
            self.bonuses.update(bonuses)
        self.prefix = None
        self.suffix = None

    def setPrefix(self, prefix):
        prefix.initialize(self)
        if prefix.wearable is self:
            if self.prefix:
                self.prefix.remove(self)
            self.prefix = prefix
        else:
            parole.warn('Failed to apply %r to %r', prefix, self)

    def setSuffix(self, suffix):
        suffix.initialize(self)
        if suffix.wearable is self:
            if self.suffix:
                self.suffix.remove(self)
            self.suffix = suffix
        else:
            parole.warn('Failed to apply %r to %r', suffix, self)

    def addAffix(self, affix):
        if isinstance(affix, Prefix):
            if self.prefix:
                raise ValueError("addAffix: Wearable already has a prefix!")
            self.setPrefix(affix)
        elif isinstance(affix, Suffixj):
            if self.suffix:
                raise ValueError("addAffix: Wearable already has a suffix!")
            self.setSuffix(affix)
        else:
            raise TypeError("addAffix: Can't determine whether prefix or "
                            "suffix: %r" % affix)

    def nounPrefix(self):
        s = super(Wearable, self).nounPrefix()
        if not self.prefix:
            return s
        return "%s %s" % (s, self.prefix.name)

    def nounSuffix(self):
        s = super(Wearable, self).nounSuffix()
        if not self.suffix:
            return s
        return "%s %s" % (self.suffix.name, s)

    def description(self):
        d = super(Wearable, self).description()
        if self.prefix:
            d = "%s\n\n%s" % (d, self.prefix.description(self))
        if self.suffix:
            d = "%s\n\n%s" % (d, self.suffix.description(self))
        return d

    def bonusString(self):
        bs = ''
        if any(self.bonuses.values()):
            bs = ' {\Violet ['
            for mod, amount in self.bonuses.iteritems():
                if not amount: continue
                bs += '%s%s%s,' % (mod[:mod.index('Mod')],
                                   amount >= 0 and '+' or '',
                                   amount)
            bs = bs[:-1] + ']}'
        return bs

    def nounAnnotation(self):
        n = super(Wearable, self).nounAnnotation()
        if self.carrier:
            if not self.wearer:
                n+= self.bonusString()
            elif self.wearSlots[0] == 'shield':
                n += "%s, {\\Lime readied" % (self.bonusString(),)
                if self.handsNeeded:
                    n += " in {\\Red %s} hand%s" % (englishNumber(self.handsNeeded),
                            (self.handsNeeded>1 and 's' or ''))
                n += "}"
            elif self.wearSlots[0] == 'light':
                #n += "%s, lit" % (self.bonusString(),)
                pass
            else:
                n += "%s, {\\Lime worn on the %s" % (self.bonusString(), self.wearerSlot)
                if self.handsNeeded:
                    n += ", using {\\Red %s} hand%s" % (englishNumber(self.handsNeeded),
                            (self.handsNeeded>1 and 's' or ''))
                n += "}"
        return n

    def handle_wear(self, event):
        """
        "wear"             (item, creature, slot)
        """
        item, creature, slot = event.args
        if item is not self:
            return

        for statname, amount in self.bonuses.iteritems():
            if not amount:
                continue
            parole.debug('%r: increasing %r by %d', self, statname,
                    amount)
            main.schedule.add(Event('increase stat', main.schedule.time,
                (creature, statname, amount), dests=[creature]))

    def handle_remove(self, event):
        """
        "remove"           (item, creature, slot)
        """
        item, creature, slot = event.args
        if item is not self:
            return

        for statname, amount in self.bonuses.iteritems():
            if not amount:
                continue
            main.schedule.sendNow(Event('decrease stat', main.schedule.time,
                (creature, statname, amount), dests=[creature]))

#=========================================

class Armor(Wearable):
    def __init__(self, name, shader, weight, wearSlots, wearEnergy,
            bonuses=None, startsVowel=False, unidDescription=None, handsNeeded=0):
        Wearable.__init__(self, name, 15, shader, weight, wearSlots,
            wearEnergy, bonuses, startsVowel, unidDescription=unidDescription,
            handsNeeded=handsNeeded)
        # TODO: Materials

    def description(self):
        desc = super(Armor, self).description()

        time = main.player.timeOfAction(self.wearEnergy)

        if self.wearer is main.player:
            desc = "%s\n\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to remove it." % (desc, main.player['speed'], time/1000.)
        else:
            desc = "%s\n\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to wear it." % (desc, main.player['speed'], time/1000.)

        #desc = "%s Its Hit Modifier is %s%d, its Base Damage is %d, and its "\
        #       "Damage Skew Modifier is %s%d." % (desc,
        #            self.bonuses['hitMod']>=0 and '+' or '',
        #            self.bonuses['hitMod'],
        #            self.damage, self.bonuses['damageSkewMod']>=0 and '+' or '',
        #            self.bonuses['damageSkewMod'])

        # TODO: stat effects
        return desc

#=========================================

class Weapon(Wearable):
    """
    A wearable that uses the verb "wield" rather than "wear", and has to-hit and
    damage stats, and construction materials and modifiers.
    """
    def __init__(self, name, shader, weight, wieldEnergy, attackEnergy,
            damage, bonuses=None, verb='swing', startsVowel=False,
            unidTemplate=None, unidMeanings=None, unidDescription=None,
            idDescription=None, projectileDamage=None, handsNeeded=1):

        Wearable.__init__(self, name, 20, shader, weight, ['weapon'],
                wieldEnergy, bonuses, startsVowel, unidTemplate=unidTemplate,
                unidMeanings=unidMeanings, unidDescription=unidDescription,
                idDescription=idDescription, handsNeeded=handsNeeded)
        self.wieldEnergy = wieldEnergy
        self.attackEnergy = attackEnergy
        self.damage = damage
        self.verb = verb
        self.projectileDamage = projectileDamage
        # TODO: Materials

    def nounAnnotation(self):
        #n = super(Weapon, self).nounAnnotation()
        n = super(Wearable, self).nounAnnotation()
        if self.carrier is main.player:
            n += " {\Violet [%s%d, D:%d%s%d, A:%d, W:%d]}" % \
                    (self.bonuses['hitMod']>=0 and '+' or '',
                    self.bonuses['hitMod'],
                    self.damage, self.bonuses['damageSkewMod']>=0 and '+' or '',
                    self.bonuses['damageSkewMod'], self.attackEnergy,
                    self.wieldEnergy)
        if self.wearer:
            n += ", {\\Lime wielded"
            if self.handsNeeded:
                n += " in {\\Red %s} hand%s" % (englishNumber(self.handsNeeded),
                        (self.handsNeeded>1 and 's' or ''))
            n += "}"
        return n

    def drawDamage(self, attacker, target):
        assert(attacker is self.wearer)
        # subclasses may want to have damage be sensitive to the identity of the
        # attacker and/or target (e.g., + vs evil)
        return random.betavariate(max(1, attacker['damageSkew']), 10) * self.damage

    def drawProjectileDamage(self, attacker, target):
        if not self.projectileDamage:
            return 0
        return random.betavariate(max(1, attacker['damageSkew']), 10) * self.projectileDamage

    def collisionMsg(self, target):
        if self.projectileDamage and target.receivesDamage:
            return "%s %s %s!" % (self.noun(definite=1, capitalized=1),
                thirdperson(self.verb), target.noun(definite=1))
        elif target.receivesDamage:
            return "%s bounces harmlessly off of %s" % (self.noun(definite=1,
                capitalized=1), target.noun(definite=1))
        else:
            return super(Weapon, self).collisionMsg(target)

    def handle_collision(self, event):
        """
        collision       (item, target, trajectory)
        """
        if event.args[0] is not self:
            return

        item, target, trajectory = event.args
        if self.projectileDamage and target.receivesDamage:
            parole.debug('Projectile weapon %r struck %r.' % (item, target))
            # schedule a "receive projectile" event, regardless of whether we hit
            main.schedule.sendNow(Event("receive projectile", main.schedule.time,
                (target, trajectory.liableParty, item), dests=[target]))

            # do some projectile damage
            damage = self.drawProjectileDamage(trajectory.liableParty,
                    target)
            main.schedule.sendNow(Event("receive damage", main.schedule.time,
                (target, trajectory.liableParty, self, damage),
                dests=[target], options={'type':'projectile'}))


    def description(self):
        desc = super(Weapon, self).description()

        time = main.player.timeOfAction(self.wieldEnergy)

        if self.wearer is main.player:
            desc = "%s\n\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to unwield it." % (desc, main.player['speed'], time/1000.)
        else:
            desc = "%s\n\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to wield it." % (desc, main.player['speed'], time/1000.)

        desc = "%s Its Hit Modifier is %s%d, its Base Damage is %d, and its "\
               "Damage Skew Modifier is %s%d." % (desc,
                    self.bonuses['hitMod']>=0 and '+' or '',
                    self.bonuses['hitMod'],
                    self.damage, self.bonuses['damageSkewMod']>=0 and '+' or '',
                    self.bonuses['damageSkewMod'])
        desc = "%s When thrown, its Base Damage is %s." % (desc,
                self.projectileDamage)

        time = main.player.timeOfAction(self.attackEnergy)
        desc = "%s\n\nAt your current speed, it would take you about %.1f "\
                "seconds to make an attack with it." % (desc, time/1000.)
        # TODO: stat efects
        return desc

#=========================================

class Affix(sim.Listener):
    """
    A "prefix" or "suffix" that modifies the behavior and/or properties of a
    Wearable in a pluggable/mixable way. A Wearable can have at most one prefix
    and one suffix.
    """
    def __init__(self, name, descriptionStr):
        self.name = name
        self.descriptionStr = descriptionStr
        self.wearable = None

    def __repr__(self):
        return '<affix %r>' % self.name
    
    def initialize(self, wearable):
        """
        Called when the affix is first "applied" to the item at creation time --
        any one-time changes to the item's properties (bonuses, etc.) should
        take place here; and for more complex affixes, this is where you should
        register to listen for events.
        """
        self.wearable = wearable

    def remove(self, wearable):
        """
        This method is the opposite of intialize. It undoes any one-time changes
        we made to the item, and lets us stop listening for events. Currently
        this never gets called, but in the future you can imagine some way for
        items to lose their affixes.
        """
        if wearable is not self.wearable:
            raise ValueError('Tried to remove affix from wrong wearable %r'\
                    % wearable)
        self.wearable = None

    def description(self, wearable):
        return self.descriptionStr % {'wearable': wearable.noun(article=False,
            annotation=False, affixes=False)}

#=========================================

class Prefix(Affix):
    pass

class Suffix(Affix):
    pass

#=========================================

class LightSource(Item):
    """
    Something that can start or stop emitting light, which follows it if it
    moves.
    """
    def __init__(self, name, layer, shader, weight, lightEnergy, lightRGB,
            lightIntensity, burnTime, startsVowel=False, unidDescription=None,
            showBurnTime=True, dousable=True, litAdjective="lit", douseAmount=5,
            handsNeeded=1):

        Item.__init__(self, name, layer, shader, weight, startsVowel,
                unidDescription=unidDescription)
        self.baseName = name
        self.on = False
        self.lightEnergy = lightEnergy
        self.lightRGB = lightRGB
        self.lightIntensity = lightIntensity
        self.lightSource = parole.map.LightSource(self.lightRGB,
                self.lightIntensity)
        self.burnTime = burnTime
        self.showBurnTime = showBurnTime
        self.enteringInventory = False
        self.snuffable = True
        self.gutterVerb = 'gutters'
        self.litAdjective = litAdjective
        self.dousable = dousable
        self.douseAmount = douseAmount
        self.handsNeeded = handsNeeded
        self.__lastAppliedMap = None
        self.__lastAppliedPos = None

    def nounPrefix(self):
        if self.on:
            self.startsVowel = False
            return self.litAdjective
        elif self.burnTime > 0:
            self.startsVowel = True
            return 'unlit'
        else:
            self.startsVowel = False
            return 'spent'

    def nounAnnotation(self):
        n = super(LightSource, self).nounAnnotation()
        if self.showBurnTime and self.burnTime > 0: 
            n += (' {\\Violet [%s left]}' % self.burnTimeString())
        if self.carrier and self.on and self.handsNeeded:
            n += ", {\\Lime held in {\\Red %s} hand%s}" % (englishNumber(self.handsNeeded),
                    (self.handsNeeded>1 and 's' or ''))
        return n


    def burnTimeString(self):
        precision = 5.0 # TODO: make precision depend on perception?
        t = self.burnTime / 60000.0
        if t < 1.0:
            return 'less than a minute'
        if t < precision:
            return 'a few minutes'
        r = t % precision
        if r <= (precision / 2.0):
            return 'about %d minutes' % round(t - r)
        return 'about %d minutes' % round(t - r + precision)

    def description(self):
        base = super(LightSource, self).description()
        
        if self.burnTime <= 0:
            return "%s\n\nIt is spent." % base

        source = None
        for obj in main.player.inventory:
            if isinstance(obj, LightSource) and obj.on and obj is not self:
                source = obj
        if not source and self.weight > main.player.maxSparkWeight:
            tooHeavy = True
        else:
            tooHeavy = False

        energy = self.on and main.player.snuffEnergy or self.lightEnergy
        if source and not self.on:
            energy /= 2

        time = main.player.timeOfAction(energy)

        if self.showBurnTime:
            desc = "%s\n\nIt looks like it has %s of light left in it." % (base,
                self.burnTimeString())
        else:
            desc = base + '\n\n'

        if self.on and self.snuffable:
            desc = "%s At your current speed (%s), it would take you about %.1f "\
                "seconds to snuff it." % (desc, main.player['speed'], time/1000.)
        elif self.on:
            desc = "%s It cannot be snuffed." % desc
        elif tooHeavy:
            desc = "%s It is too big to light with your flint; you'll need a "\
                   "torch or something." % desc
        else:
            desc = "%s At your current speed (%s), it would take you about "\
                "%.1f seconds to light it with your %s." % (desc,
                main.player['speed'], time/1000., 
                source and source.noun(article=0) or 'flint')
        return desc

    def activate(self, map, pos):
        if self.on:
            return
        self.on = True
        self.lightSource.apply(map, pos)
        self.burnEvent = Event('burn', main.schedule.time + 1000, (self,),
                dests=[self])
        main.schedule.add(self.burnEvent)
        main.schedule.listen('translocate', self)
        main.schedule.listen('leave tile', self)
        main.schedule.listen('enter tile', self)

        self.__handleChange(map)

    def deactivate(self, map):
        if not self.on:
            return
        self.on = False
        self.lightSource.remove(map)
        if self.burnEvent in main.schedule:
            main.schedule.remove(self.burnEvent)
        main.schedule.unlisten('translocate', self)
        main.schedule.unlisten('leave tile', self)
        main.schedule.unlisten('enter tile', self)

        self.__handleChange(map)

    def tryDouse(self, liquid):
        # TODO: handle oil
        if not self.dousable:
            return False
        if liquid.amount > self.douseAmount:
            if self.pos:
                if visible(self) and self.on:
                    main.transient("%s is doused by %s." % \
                        (self.noun(definite=1, capitalized=1),
                            liquid.noun(definite=1)), self.pos)
                self.deactivate(self.parentTile.map)
                return True
            elif self.carrier:
                raise NotImplementedError
        return False

    def __handleChange(self, map):
        map.update()
        if main.mapFrame.fovObj and not main.player.dead:
            main.mapFrame.touchFOV()

        if self.carrier and self.carrier.parentTile:
            if self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                self.carrier.goUnblind()
            else:
                self.carrier.goBlind()

    def listen(self, event, superListen=True):
        """
        Handles:
            "activate light"    (obj)
            "deactivate light"  (obj)
            "toggle light"      (obj)
            "update light"      (obj, lightRGB, lightIntensity)
            "translocate"       (obj, pos, map)
            "enter tile"        (obj, pos, map)
            "leave tile"        (obj, pos)
            "burn"              (lightsource)
            "leave inventory"   (item, creature)
            "enter inventory"   (item, creature)
        """
        map = None
        pos = None
        if self.parentTile:
            map = self.parentTile.map
            pos = self.pos
        elif self.carrier:
            if self.carrier.parentTile:
                map = self.carrier.parentTile.map
                pos = self.carrier.pos
        if not map:
            # assume it's the currently visible one
            map = main.mapFrame.getMap()


        # allow caller to prevent superclasses from listening, to support
        # multiple inheritance with LightSources.
        if superListen:
            Item.listen(self, event)

        if event.id == "leave tile":
            obj, pos = event.args
            if obj is self or obj is self.carrier:
                self.enteringInventory = False

        #elif event.id == 'leave inventory':
        #    item, creature = event.args
        #    if item is self:
        #        if self.on:
        #            if not map:
        #                map = creature.parentTile.map
        #            self.lightSource.remove(map)
        #            map.update()

        elif event.id == 'burn' and event.args[0] is self:
            self.burnTime -= 1000
            if self.burnTime <= 0:
                self.burnTime = 0
                main.schedule.add(Event('deactivate light', main.schedule.time,
                    (self,), dests=[self]))
            else:
                self.burnEvent = Event('burn', main.schedule.time + 1000, (self,),
                        dests=[self])
                main.schedule.add(self.burnEvent)
                if self.burnTime <= 10000 and \
                        (visible(self) or visible(self.carrier)):
                    main.transient("%s %s like it's about to go out." % \
                            (self.noun(definite=1, capitalized=1),
                                self.gutterVerb), pos)

        elif event.id == "activate light" and event.args[0] is self:
            if self.on:
                # already on
                return
            if pos is None:
                parole.warn('activate light: pos is None.')
                return
            if visible(self) or visible(self.carrier):
                main.transient('%s lights up.' % self.noun(definite=1,
                    capitalized=1), pos)
            self.activate(map, pos)

        elif event.id == "deactivate light" and event.args[0] is self:
            if not self.on:
                # already off
                return
            if pos is None:
                parole.warn('deactivate light: pos is None.')
                return
            if visible(self) or visible(self.carrier):
                main.transient('%s goes out.' % self.noun(definite=1,
                    capitalized=1), pos)
            if self.carrier:
                self.carrier.releaseHands(self.handsNeeded)
            self.deactivate(map)

        elif event.id == 'toggle light' and event.args[0] is self:
            if self.on:
                main.schedule.add(Event('deactivate light', main.schedule.time,
                    (self,), dests=[self]))
            else:
                main.schedule.add(Event('activate light', main.schedule.time,
                    (self,), dests=[self]))

        elif event.id == 'update light':
            light, lightRGB, lightIntensity = event.args
            if light is not self:
                return
            changed = False
            if lightIntensity != self.lightIntensity:
                changed = True
                if self.on:
                    self.lightSource.remove(map)
                self.lightSource.setIntensity(lightIntensity)
                if self.on and pos:
                    self.lightSource.apply(map, pos)
            if lightRGB != self.lightRGB:
                changed = True
                self.lightSource.setRGB(lightRGB)

            if changed:
                self.__handleChange(map)

            self.lightRGB, self.lightIntensity = lightRGB, lightIntensity

        elif event.id == "translocate":
            if event.canceled:
                return
            obj, epos, emap = event.args
            if obj is self or obj is self.carrier:
                #parole.debug('******************* map = %r, emap = %r', map, emap)
                #assert(emap is map)
                if self.on:
                    self.lightSource.remove(map)
                    self.lightSource.apply(emap, epos)
                    emap.update()
                    self.__lastAppliedMap = emap
                    self.__lastAppliedPos = epos
            if self.carrier and self.carrier.parentTile:
                if self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                    self.carrier.goUnblind()

        elif event.id == 'enter tile':
            if event.canceled:
                return
            obj, epos, emap = event.args
            if self.__lastAppliedMap is emap and self.__lastAppliedPos == epos:
                return
            if obj is self or obj is self.carrier:
                #parole.debug('******************* map = %r, emap = %r', map, emap)
                #assert(emap is map)
                if self.on:
                    self.lightSource.remove(map)
                    self.lightSource.apply(emap, epos)
                    emap.update()
                    self.__lastAppliedMap = emap
                    self.__lastAppliedPos = epos
            if self.carrier and self.carrier.parentTile:
                if self.carrier.tileIsBrightEnough(self.carrier.parentTile):
                    self.carrier.goUnblind()


        elif event.id == 'enter inventory':
            item, creature = event.args
            if item is self:
                if self.on:
                    if not map:
                        map = creature.parentTile.map
                    #parole.debug('***************************** applying')
                    #self.lightSource.apply(map, creature.pos)
                    #map.update()
                    #if main.mapFrame.fovObj:
                    #    main.mapFrame.touchFOV()
                    self.enteringInventory = True

        elif event.id == "splash":
            puddle, pos, map = event.args
            if map[pos] is self.parentTile or \
                    (self.carrier and map[pos] is self.carrier.parentTile):
                self.tryDouse(puddle)
            return


#=========================================

class FlamingScroll(LightSource):
    """
    A scroll that has been lit on fire. The actual Scroll object gets removed
    and replaced by an instance of this.
    """
    def __init__(self, map, pos):
        super(FlamingScroll, self).__init__('flaming scroll',
                50, parole.map.AsciiTile('*', colors['Yellow']),
                Scroll.scrollWeight, 2500,
                (255,255,255), 0.5, 30000,
                unidDescription="A scroll that has been lit on fire.")
        # starts lit
        self.activate(map, pos)

    def updateName(self):
        if self.burnTime <= 0:
            self.name = 'ash'
            self.startsVowel = True
            self.massNoun = True
            self.shader = parole.map.AsciiTile('%', colors['DarkGray'])
            self.layer = 5
            self.weight *= 0.5
        else:
            self.name = self.baseName

    def nounPrefix(self):
        return ''

    def listen(self, event):
        super(FlamingScroll, self).listen(event)
        map = None
        pos = None
        if self.parentTile:
            map = self.parentTile.map
            pos = self.pos
        elif self.carrier:
            if self.carrier.parentTile:
                map = self.carrier.parentTile.map
                pos = self.carrier.pos

        if event.id == "deactivate light" and event.args[0] is self:
            if visible(self) or visible(self.carrier):
                self.burnTime = 1
                self.updateName()
                main.transient('%s crumbles to ash.' % self.noun(definite=1,
                    capitalized=1), pos)
                self.burnTime = 0
                self.updateName()
                if self.carrier:
                    self.carrier.inventory.remove(self)
                    map[self.carrier.pos].add(self)
                    self.carrier = None
                    main.schedule.sendNow(Event('enter tile', main.schedule.time,
                            (self, self.pos, map), dests=[self]))


#=========================================

class Scroll(Item):
    """
    An item that can be read.
    """

    scrollWeight = 0.2  # pounds
    lightEnergy = 2500

    classColors = {} # class (subclass of Scroll) -> rgb

    liquidSoakAmount = 5 # how much liquid it takes to soak a scroll

    unidDescription = \
        "A rolled fragment of vellum, the outer surface of which is neatly "\
        "labeled in black ink. You do not know the language in which it is "\
        "written, but you recognize the characters of the script, and, if "\
        "pressed, could probably pronounce them with some competence. When "\
        "unrolled, the inner surface is revealed to contain a brief passage "\
        "of text in the same mysterious language. The whole thing bears a "\
        "remarkable resemblance to the \"magical scrolls\" described by "\
        "certain vulgar legends, in which it is said that reading aloud the "\
        "contents of such a scroll can have uncanny consequences."

    def __init__(self, name, char="?", unidMeanings=None, idDescription=None):
        unidTemplate = unidMeanings and 'scroll labeled {\\%(fg_rgb)s "%(form)s"}' or \
                'unlabeled scroll'
        rgb = None
        if self.__class__ not in Scroll.classColors:
            while (not rgb) or rgb in Scroll.classColors.values():
                rgb = (random.randint(0,255), random.randint(0,255),
                        random.randint(0,255))
            Scroll.classColors[self.__class__] = rgb
        else:
            rgb = Scroll.classColors[self.__class__]

        Item.__init__(self, 'scroll of '+name, 10, parole.map.AsciiTile(char, 
            rgb), Scroll.scrollWeight, unidTemplate=unidTemplate,
            unidMeanings=unidMeanings, unidDescription=Scroll.unidDescription,
            idDescription=idDescription)

    def description(self):
        return super(Scroll, self).description() + \
                "\n\nThe label reads: \"%s\"." % self.unidForm() + \
                "\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to read it." % (main.player['speed'],
                        main.player.timeOfAction(main.player.readEnergy)/1000.)

    def trySoak(self, liquid):
        if liquid.amount > self.liquidSoakAmount:
            if self.pos:
                if visible(self):
                    main.transient("%s is completely soaked by %s." % \
                        (self.noun(definite=1, capitalized=1),
                            liquid.noun(definite=1)), self.pos)
                t = self.parentTile
                t.remove(self)
                t.add(SoggyScroll(liquid))
                return True
            elif self.carrier:
                # TODO: soaking in inventory
                raise NotImplementedError
        return False

    def handle_splash(self, event):
        """
        "splash"            (puddle, pos, map)
        """
        puddle, pos, map = event.args
        if map[pos] is self.parentTile:
            self.trySoak(puddle)
        return

    def handle_enter_tile(self, event):
        """
        "enter tile"        (obj, dest, destMap)
        """
        if event.args[0] is self:
            dest, destMap = event.args[1:]
            for obj in destMap[dest]:
                if isinstance(obj, sim.Puddle) and self.trySoak(obj):
                    return

    def read(self, reader):
        parole.error('Unimplemented scroll read: %r', self)
        pass

#=========================================

class SoggyScroll(Item):
    """
    A Scroll that has been soaked in some liquid and rendered useless.
    """
    
    def __init__(self, liquid):
        Item.__init__(self, '%s-soaked scroll' % liquid.baseName, 10, 
                parole.map.AsciiTile('?', (64,64,64)), Scroll.scrollWeight,
                unidDescription="A scroll that has been soaked in %s, rendering "
                "it soggy and illegible." % liquid.baseName)
        self.liquid = liquid

#=========================================

class PotionPuddle(sim.Puddle):
    def __init__(self, amount, big=False, srcPotion=None):
        assert(srcPotion)
        fg_rgb = srcPotion.shader.rgb
        if big:
            name = random.choice(['puddle', 'pool', 'splash'])
            char = ' '
            bg_rgb = srcPotion.shader.rgb
        else:
            name = random.choice(['splatter', 'spattering', 'smear', 'streak',
                'smattering'])
            char = random.choice(['.', ',', ';'])
            bg_rgb = None

        super(PotionPuddle, self).__init__(amount, name + ' of strange liquid',
                'strange liquid', char, fg_rgb, bg_rgb)
        self.big = big
        self.srcPotion = srcPotion

#=========================================

class Potion(Item):
    """
    Quaff one of these for a great time!
    """
    potionWeight = 0.5  # pounds
    splatterAmount = 10
    splatterDist = 3
    puddleClass = PotionPuddle

    classColors = {} # class (subclass of Potion) -> rgb

    unidDescription = \
      "A small glass flask containing roughly half a pint of an opaque liquid. "\
      "It bears a label written in the perplexing calligraphy of an unfamiliar "\
      "tongue. A faint chemical odor wafts from it... here goes nothing."

    def __init__(self, name, char="!", unidMeanings=None, idDescription=None):
        unidTemplate = unidMeanings and 'potion labeled {\\%(fg_rgb)s "%(form)s"}' or \
                'unlabeled potion'
        rgb = None
        if self.__class__ not in Potion.classColors:
            while (not rgb) or rgb in Potion.classColors.values():
                rgb = (random.randint(0,255), random.randint(0,255),
                        random.randint(0,255))
            Potion.classColors[self.__class__] = rgb
        else:
            rgb = Potion.classColors[self.__class__]

        Item.__init__(self, 'potion of '+name, 20, parole.map.AsciiTile(char, 
            rgb), Potion.potionWeight, unidTemplate=unidTemplate,
            unidMeanings=unidMeanings, unidDescription=Potion.unidDescription,
            idDescription=idDescription)

    def description(self):
        return super(Potion, self).description() + \
                "\n\nThe label reads: \"%s\"." % self.unidForm() + \
                "\nAt your current speed (%s), it would take you about %.1f "\
                "seconds to quaff it." % (main.player['speed'],
                        main.player.timeOfAction(main.player.quaffEnergy)/1000.)

    def collisionMsg(self, other):
        return '%s shatters against %s.' % (self.noun(definite=1,
            capitalized=1), other.noun(definite=1))

    def handle_collision(self, event):
        """
        collision       (item, target, trajectory)
        """
        if event.args[0] is not self:
            return

        item, target, trajectory = event.args # item is self
        slope = difference(trajectory.path[-1], trajectory.path[-2])
        
        #if isinstance(target, Creature):
        #    # TODO: shatter on a creature
        #    pass
        #else:
            # shatter on something else (possibly floor)
        splatterTile = target.parentTile.hasMoveBlocker() and \
                self.parentTile or target.parentTile
        splatter = sim.Splatter(self.puddleClass, self.splatterAmount)
        splatter.apply(splatterTile, slope, self.splatterDist,
                srcPotion=self)

        if self.pos:
            main.schedule.add(Event('leave tile', main.schedule.time,
                    (self, self.pos), dests=[self]))
            self.parentTile.remove(self)
                
    def quaff(self, quaffer):
        parole.error('Unimplemented potion quaffed: %r', self)

#=========================================

